package ru.maxmatveev.dyndao.impl.hibernate;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.*;
import org.hibernate.metadata.ClassMetadata;
import ru.maxmatveev.dyndao.*;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;

/**
 * Hibernate implementation.
 *
 * @author Max Matveev
 *         Date: Dec 20, 2008
 *         Time: 4:29:49 PM
 */
@SuppressWarnings({"MissingClassJavaDoc"})
public class DynamicDaoImpl<T, K extends Serializable> implements DynamicDao<T, K>, MethodInterceptor {

    /**
     * Field entityClass.
     */
    private final Class<T> entityClass;


    /**
     * Unique generated id.
     */
    private String id;

    /**
     * ID field name. Automatically set on first usage.
     */
    private String idName = null;

    /**
     * Constructor DynamicDaoImpl creates a new DynamicDaoImpl instance.
     *
     * @param entityClass of type Class<T>
     */
    public DynamicDaoImpl(Class<T> entityClass) {
        this.entityClass = entityClass;
        id = "_" + (System.nanoTime() / Integer.MAX_VALUE + 31 * this.entityClass.hashCode());
    }

    /**
     * @inheritDoc
     */
    public T delete(T entity) {
        ThreadLocalSessionFactory.getSession().delete(entity);
        return entity;
    }

    /**
     * @inheritDoc
     */
    @SuppressWarnings({"unchecked"})
    public T getById(K id) {
        return (T) ThreadLocalSessionFactory.getSession().get(entityClass, id);
    }

    /**
     * @inheritDoc
     */
    @SuppressWarnings({"unchecked"})
    public List<T> getAll() {
        return ThreadLocalSessionFactory.getSession().createCriteria(entityClass).list();
    }

    /**
     * @inheritDoc
     */
    public T save(T entity) {
        ThreadLocalSessionFactory.getSession().save(entity);
        return entity;
    }


    /**
     * Get DynDao parameter annotation.
     *
     * @param annotations annotations array to check
     * @return Annotation, or null if annotation is not present
     */
    private Annotation getParamtererAnnotation(Annotation[] annotations) {
        int dynDaoAnnotationCount = 0;
        Annotation parameterAnnotation = null;
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == Ordering.Criteria.class ||
                    annotation.annotationType() == Pagination.Paginate.class ||
                    annotation.annotationType() == Restriction.ParentId.class ||
                    annotation.annotationType() == Restriction.ParentField.class ||
                    annotation.annotationType() == Restriction.Parent.class ||
                    annotation.annotationType() == Restriction.Field.class ||
                    annotation.annotationType() == Restriction.Restrict.class) {
                dynDaoAnnotationCount++;
                parameterAnnotation = annotation;
            }
        }
        if (dynDaoAnnotationCount > 1) {
            throw new IllegalArgumentException("More than one DynDao annotation are present on one parameter");
        }
        return parameterAnnotation;
    }

    /**
     * Dynamic proxy interceptor.
     * Implements dynamic method implementation.
     *
     * @param object      Object which method is intercepted
     * @param method      Method being intercepted
     * @param arguments   Method arguments
     * @param methodProxy Method proxy
     * @return Object rusult
     * @throws Throwable when unexpected error occurres
     */
    @SuppressWarnings({"unchecked"})
    public Object intercept(Object object, Method method, Object[] arguments, MethodProxy methodProxy) throws Throwable {
        if (!Modifier.isAbstract(method.getModifiers())) {
            return methodProxy.invokeSuper(object, arguments);
        }
        Session session = ThreadLocalSessionFactory.getSession();

        boolean count = method.isAnnotationPresent(Count.class);
        boolean byParent = false;
        boolean restrict = false;
        boolean sorting = false;
        boolean pagination = false;

        Restriction parentRestrictionValue = null;
        Restriction.Parent parentRestriction = null;

        Object parentFieldRestrictionValue = null;
        Restriction.ParentField parentFieldRestriction = null;

        Serializable parentIdRestrictionValue = null;
        Restriction.ParentId parentIdRestriction = null;

        Restriction restrictionValue = null;
        Restriction.Restrict restriction = null;

        Object restrictionFieldValue = null;
        Restriction.Field restrictionField = null;

        Ordering sortingCriteriaValue = null;
        Ordering.Criteria sortingCriteria = null;

        Ordering.Field sortingField;

        Pagination paginationValue = null;

        if ((sortingField = method.getAnnotation(Ordering.Field.class)) != null) {
            if (count) {
                throw new IllegalArgumentException("Sorting should not be used with @Count annotated methods");
            }
            sorting = true;
        }
        for (int i = 0; i < method.getParameterAnnotations().length; i++) {
            Annotation annotation;
            if ((annotation = getParamtererAnnotation(method.getParameterAnnotations()[i])) != null) {
                if (annotation.annotationType() == Restriction.Parent.class) {
                    if (byParent) {
                        throw new IllegalArgumentException("There is more than one parent filtering argument in method definition.");
                    }
                    if (!(arguments[i] instanceof Restriction)) {
                        throw new IllegalArgumentException("Annotation @Restriction.Parent should be used only on Resctriction type parameters.");
                    }
                    parentRestriction = (Restriction.Parent) annotation;
                    parentRestrictionValue = (Restriction) arguments[i];
                    byParent = true;
                } else if (annotation.annotationType() == Restriction.ParentField.class) {
                    if (byParent) {
                        throw new IllegalArgumentException("There is more than one parent filtering argument in method definition.");
                    }
                    parentFieldRestriction = (Restriction.ParentField) annotation;
                    parentFieldRestrictionValue = arguments[i];
                    byParent = true;
                } else if (annotation.annotationType() == Restriction.ParentId.class) {
                    if (byParent) {
                        throw new IllegalArgumentException("There is more than one parent filtering argument in method definition.");
                    }
                    if (!(arguments[i] instanceof Serializable)) {
                        throw new IllegalArgumentException("Annotation @Restriction.ParentId should be used only on Serializable type parameters.");
                    }
                    parentIdRestriction = (Restriction.ParentId) annotation;
                    parentIdRestrictionValue = (Serializable) arguments[i];
                    byParent = true;
                } else if (annotation.annotationType() == Restriction.Restrict.class) {
                    if (restrict) {
                        throw new IllegalArgumentException("There is more than one filtering argument in method definition.");
                    }
                    if (!(arguments[i] instanceof Restriction)) {
                        throw new IllegalArgumentException("Annotation @Restriction.Restrict should be used only on Resctriction type parameters.");
                    }
                    restriction = (Restriction.Restrict) annotation;
                    restrictionValue = (Restriction) arguments[i];
                    restrict = true;
                } else if (annotation.annotationType() == Restriction.Field.class) {
                    if (restrict) {
                        throw new IllegalArgumentException("There is more than one filtering argument in method definition.");
                    }
                    restrictionField = (Restriction.Field) annotation;
                    restrictionFieldValue = arguments[i];
                    restrict = true;
                } else if (annotation.annotationType() == Ordering.Criteria.class) {
                    if (sorting) {
                        throw new IllegalArgumentException("There is more than one ordering argument in method definition.");
                    }
                    if (count) {
                        throw new IllegalArgumentException("Sorting should not be used with @Count annotated methods");
                    }
                    if (!(arguments[i] instanceof Ordering)) {
                        throw new IllegalArgumentException("Annotation @Ordering.Criteria should be used only on Ordering type parameters.");
                    }
                    sortingCriteria = (Ordering.Criteria) annotation;
                    sortingCriteriaValue = (Ordering) arguments[i];
                    sorting = true;
                } else if (annotation.annotationType() == Pagination.Paginate.class) {
                    if (pagination) {
                        throw new IllegalArgumentException("There is more than one pagination argument in method definition.");
                    }
                    if (count) {
                        throw new IllegalArgumentException("Pagination should not be used with @Count annotated methods");
                    }
                    if (!(arguments[i] instanceof Pagination)) {
                        throw new IllegalArgumentException("Annotation @Pagination.Paginate should be used only on Pagination type parameters.");
                    }
                    paginationValue = (Pagination) arguments[i];
                    pagination = true;
                }

            }
        }
        if (byParent) {
            DetachedCriteria detachedCriteria = null;
            Criteria criteria;
            if (parentRestriction != null) {
                detachedCriteria = DetachedCriteria.forClass(parentRestriction.parentClass());
                if (idName == null) {
                    ClassMetadata parentMetadata = session.getSessionFactory().getClassMetadata(parentRestriction.parentClass());
                    idName = parentMetadata.getIdentifierPropertyName();
                }
                if (parentRestrictionValue != null) {
                    Criterion criterion = getCriterion(parentRestrictionValue);
                    if (criterion != null) {
                        detachedCriteria.add(criterion);
                    }
                }
                detachedCriteria = detachedCriteria.createCriteria(parentRestriction.fieldName(), id);
            } else if (parentFieldRestriction != null) {
                detachedCriteria = DetachedCriteria.forClass(parentFieldRestriction.parentClass());
                if (idName == null) {
                    ClassMetadata parentMetadata = session.getSessionFactory().getClassMetadata(parentFieldRestriction.parentClass());
                    idName = parentMetadata.getIdentifierPropertyName();
                }
                switch (parentFieldRestriction.filterFieldComparison()) {
                    case EQUALS:
                        detachedCriteria.add(Restrictions.eq(parentFieldRestriction.filterFieldName(), parentFieldRestrictionValue));
                        break;
                    case IEQUALS:
                        detachedCriteria.add(Restrictions.ilike(parentFieldRestriction.filterFieldName(), (String) parentFieldRestrictionValue, MatchMode.EXACT));
                        break;
                    case LIKE:
                        detachedCriteria.add(Restrictions.like(parentFieldRestriction.filterFieldName(), (String) parentFieldRestrictionValue, MatchMode.ANYWHERE));
                        break;
                    case ILIKE:
                        detachedCriteria.add(Restrictions.ilike(parentFieldRestriction.filterFieldName(), (String) parentFieldRestrictionValue, MatchMode.ANYWHERE));
                        break;
                }
                detachedCriteria = detachedCriteria.createCriteria(parentFieldRestriction.fieldName(), id);
            } else if (parentIdRestriction != null) {
                detachedCriteria = DetachedCriteria.forClass(parentIdRestriction.parentClass());
                detachedCriteria.add(Restrictions.idEq(parentIdRestrictionValue));
                if (idName == null) {
                    ClassMetadata parentMetadata = session.getSessionFactory().getClassMetadata(parentIdRestriction.parentClass());
                    idName = parentMetadata.getIdentifierPropertyName();
                }
                detachedCriteria = detachedCriteria.createCriteria(parentIdRestriction.fieldName(), id);
            }
            if (restrict) {
                if (restrictionField != null) {
                    switch (restrictionField.comparison()) {
                        case EQUALS:
                            detachedCriteria.add(Restrictions.eq(restrictionField.fieldName(), restrictionFieldValue));
                            break;
                        case IEQUALS:
                            detachedCriteria.add(Restrictions.ilike(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.EXACT));
                            break;
                        case LIKE:
                            detachedCriteria.add(Restrictions.like(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.ANYWHERE));
                            break;
                        case ILIKE:
                            detachedCriteria.add(Restrictions.ilike(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.ANYWHERE));
                            break;
                    }
                } else if (restriction != null) {
                    if (restrictionValue != null) {
                        Criterion criterion = getCriterion(restrictionValue);
                        if (criterion != null) {
                            detachedCriteria.add(criterion);
                        }
                    }
                }
            }
            if (count) {
                return detachedCriteria.getExecutableCriteria(session).setProjection(Projections.countDistinct(id + "." + idName)).uniqueResult();
            } else {
                criteria = session.createCriteria(entityClass);
                criteria.add(Property.forName(idName).in(detachedCriteria.setProjection(Projections.groupProperty(idName))));
                if (sorting) {
                    if (sortingField != null) {
                        switch (sortingField.direction()) {
                            case ASC:
                                criteria.addOrder(Order.asc(sortingField.fieldName()));
                                break;
                            case DESC:
                                criteria.addOrder(Order.desc(sortingField.fieldName()));
                                break;
                        }
                    } else if (sortingCriteria != null) {
                        while (sortingCriteriaValue != null) {
                            switch (sortingCriteriaValue.getDirection()) {
                                case ASC:
                                    criteria.addOrder(Order.asc(sortingCriteriaValue.getField()));
                                    break;
                                case DESC:
                                    criteria.addOrder(Order.desc(sortingCriteriaValue.getField()));
                                    break;
                            }
                            sortingCriteriaValue = sortingCriteriaValue.getChild();
                        }
                    }
                }
                if (pagination) {
                    if (paginationValue != null) {
                        criteria.setFirstResult(paginationValue.getFirstResult());
                        criteria.setMaxResults(paginationValue.getMaxResults());
                    }
                }
                return criteria.list();
            }
        } else {
            Criteria criteria = session.createCriteria(entityClass);
            if (restrict) {
                if (restrictionField != null) {
                    switch (restrictionField.comparison()) {
                        case EQUALS:
                            criteria.add(Restrictions.eq(restrictionField.fieldName(), restrictionFieldValue));
                            break;
                        case IEQUALS:
                            criteria.add(Restrictions.ilike(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.EXACT));
                            break;
                        case LIKE:
                            criteria.add(Restrictions.like(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.ANYWHERE));
                            break;
                        case ILIKE:
                            criteria.add(Restrictions.ilike(restrictionField.fieldName(), (String) restrictionFieldValue, MatchMode.ANYWHERE));
                            break;
                    }
                } else if (restriction != null) {
                    if (restrictionValue != null) {
                        Criterion criterion = getCriterion(restrictionValue);
                        if (criterion != null) {
                            criteria.add(criterion);
                        }
                    }
                }
            }
            if (sorting) {
                if (sortingField != null) {
                    switch (sortingField.direction()) {
                        case ASC:
                            criteria.addOrder(Order.asc(sortingField.fieldName()));
                            break;
                        case DESC:
                            criteria.addOrder(Order.desc(sortingField.fieldName()));
                            break;
                    }
                } else if (sortingCriteria != null) {
                    while (sortingCriteriaValue != null) {
                        switch (sortingCriteriaValue.getDirection()) {
                            case ASC:
                                criteria.addOrder(Order.asc(sortingCriteriaValue.getField()));
                                break;
                            case DESC:
                                criteria.addOrder(Order.desc(sortingCriteriaValue.getField()));
                                break;
                        }
                        sortingCriteriaValue = sortingCriteriaValue.getChild();
                    }
                }
            }
            if (pagination) {
                if (paginationValue != null) {
                    criteria.setFirstResult(paginationValue.getFirstResult());
                    criteria.setMaxResults(paginationValue.getMaxResults());
                }
            }
            if (count) {
                return criteria.setProjection(Projections.rowCount()).uniqueResult();
            }
            if (Collection.class.isAssignableFrom(method.getReturnType())) {
                return criteria.list();
            } else {
                return criteria.uniqueResult();
            }
        }
    }


    /**
     * Build request criterion.
     *
     * @param restriction Restriction object to build criterion on
     * @return Criterion
     */
    private Criterion getCriterion(Restriction restriction) {
        if (restriction == null) {
            return null;
        }
        if (restriction.getLogicalOp() != null) {
            switch (restriction.getLogicalOp()) {
                case NOT:
                    return Restrictions.not(getCriterion(restriction.getChild1()));
                case AND:
                    return Restrictions.and(getCriterion(restriction.getChild1()), getCriterion(restriction.getChild2()));
                case OR:
                    return Restrictions.or(getCriterion(restriction.getChild1()), getCriterion(restriction.getChild2()));

            }
        } else {
            switch (restriction.getComparison()) {
                case EQUALS:
                    return Restrictions.eq(restriction.getField(), restriction.getValue());
                case IEQUALS:
                    return Restrictions.ilike(restriction.getField(), (String) restriction.getValue(), MatchMode.EXACT);
                case LIKE:
                    return Restrictions.like(restriction.getField(), (String) restriction.getValue(), MatchMode.ANYWHERE);
                case ILIKE:
                    return Restrictions.ilike(restriction.getField(), (String) restriction.getValue(), MatchMode.ANYWHERE);
            }
        }
        return null;
    }
}
